/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.taglib.core.menu;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.model.core.Page;
import com.blandware.atleap.service.core.PageManager;
import com.blandware.atleap.webapp.exception.MenuUtilException;
import com.blandware.atleap.webapp.menu.MenuComponent;
import com.blandware.atleap.webapp.struts.ContentTilesRequestProcessor;
import com.blandware.atleap.webapp.util.core.CacheUtil;
import com.blandware.atleap.webapp.util.core.MenuUtil;
import com.blandware.atleap.webapp.util.core.WebappUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.GenericValidator;
import org.apache.struts.Globals;
import org.apache.struts.taglib.TagUtils;
import org.apache.struts.tiles.ComponentDefinition;
import org.apache.struts.tiles.TilesUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

/**
 * <p>Replacement of DisplayMenuTag. Can get menu items from persistent storage.
 * Menu may be put into specified variable as a solid MenuComponent instance
 * or as a list of such fragments (if menu needs to be split into a number
 * of slices - for example, for multi-column menu).</p>
 * <p>
 * Allowed attributes are:
 * <ul>
 * <li>
 * <b>name</b> - required - name of menu component that needs to be obtained.
 * If <b>var</b> is not specified, this will be used instead of it too.
 * </li>
 * <li>
 * <b>definition</b> - name of definition for which to obtain menu items. By
 * default it is definition of currently processed page.
 * </li>
 * <li>
 * <b>uri</b> - URI of page for which to obtain menu items. By default it is URI
 * of currently processed page.
 * </li>
 * <li>
 * <b>locale</b> - locale to use. By default it will be taken from session
 * </li>
 * <li>
 * <b>addLocaleSuffix</b> - whether to add locale suffixes to menu locations.
 * Default is "true".
 * </li>
 * <li>
 * <b>checkRoles</b> - whether to check roles. If "true", only menu items which
 * are allowed for current user will be put to result, else menu structure will
 * remain untouched.
 * Default is "false".
 * </li>
 * <li>
 * <b>hreflangFilter</b> - whether to filter by hreflang. If "true", only menu
 * items which hreflang is empty or matches current language will be put to
 * result, else menu structure will remain untouched.
 * Default is "false".
 * </li>
 * <li>
 * <b>slices</b> - number of slices to which to split menu. If not specified,
 * single MenuConponent instance will be put to variable as a result. Otherwise,
 * menu will be previously divided into a specified number of fragments
 * (<em>slices</em>). Slices will have approximately equal number of top-level
 * menu items. If total number of menu items in a whole menu is not divisible
 * by number of slices, first N slices will be one element longer, where N is a
 * remainder after dividing number of top-level elements by number of slices.
 * </li>
 * <li>
 * <b>var</b> - name of variable that will accept loaded menu component. If not
 * specified, <b>name</b> will be used.
 * </li>
 * <li>
 * <b>scope</b> - scope of variable to save loaded menu component
 * </li>
 * </ul>
 * </p>
 * <p><a href="PrepareMenuTag.java.html"><i>View Source</i></a></p>
 *
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.15 $ $Date: 2007/05/04 12:37:08 $
 * @jsp.tag name="prepareMenu"
 * body-content="empty"
 */
public class PrepareMenuTag extends SimpleTagSupport {

	protected transient final Log log = LogFactory.getLog(PrepareMenuTag.class);

	protected ApplicationContext applicationCtx = null;

	/**
	 * Definition name in database. By default it is definition of current processed page
	 */
	protected String definition = null;

	/**
	 * URI of the page in database. By default it is URI of currently processed page
	 */
	protected String uri = null;

	/**
	 * Locale to use. By default it will be taken from session
	 */
	protected String locale = null;

	/**
	 * Holds value of property name.
	 */
	protected String name;

	/**
	 * Name of variable to use instead of value specified in <code>name</code> attribute to save prepared menu component
	 */
	protected String var;

	/**
	 * Scope to export menu in
	 */
	protected String scope;

	/**
	 * Whether or not to add locale suffixes to menu locations
	 */
	protected Boolean addLocaleSuffix = Boolean.TRUE;

    /**
     * Whether to check roles and output only menu items for which user has
     * sufficient rights
     */
    protected Boolean checkRoles = Boolean.FALSE;

    /**
     * Whether to check hreflang and output only menu items for which hreflang
     * is empty or matches current language
     */
    protected Boolean hreflangFilter = Boolean.FALSE;

    /**
     * Number of slices to which to divide result
     */
    protected Integer slices;

	/**
	 * Returns name of menu item
	 *
	 * @return name of menu item
	 * @jsp.attribute required="true"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="The name of menu item"
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * Sets name of menu item
	 *
	 * @param name New value of property name.
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
     * Returns URI for which menu items will be loaded
     *
     * @return URI for which menu items will be loaded
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="URI of the page in database. By default it is URI of currently processed page"
	 */
	public String getUri() {
		return uri;
	}

    /**
     * Sets URI for which menu items will be loaded
     *
     * @param uri URI for which menu items will be loaded
     */
	public void setUri(String uri) {
		this.uri = uri;
	}

	/**
     * Returns name of definition for which menu items will be loaded
     *
     * @return name of definition for which menu items will be loaded
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Definition name in persistence storage. By default it is definition of currently processed page"
	 */
	public String getDefinition() {
		return definition;
	}

    /**
     * Sets name of definition for which menu items will be loaded
     *
     * @param definition name of definition for which menu items will be loaded
     */
	public void setDefinition(String definition) {
		this.definition = definition;
	}

	/**
     * Returns locale that will be used when loading menu items
     *
     * @return locale
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="Locale to use. By default it will be taken from session"
	 */
	public String getLocale() {
		return locale;
	}

    /**
     * Sets locale that will be used when loading menu items
     *
     * @param locale locale to set
     */
	public void setLocale(String locale) {
		this.locale = locale;
	}

	/**
     * Returns name of variable that will accept loaded menu
     *
	 * @return variable name
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Name of variable to use instead of value specified in 'name' attribute to save prepared menu component"
	 */
	public String getVar() {
		return var;
	}

    /**
     * Sets name of variable that will accept loaded menu
     *
     * @param var name of variable to set
     */
	public void setVar(String var) {
		this.var = var;
	}

	/**
     * Returns scope of variable that will accept loaded menu
     *
     * @return variable scope
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.String"
	 * description="Scope to export variable to"
	 */
	public String getScope() {
		return scope;
	}

    /**
     * Sets scope of variable that will accept loaded menu
     *
     * @param scope variable scope to set
     */
	public void setScope(String scope) {
		this.scope = scope;
	}

	/**
     * Returns whether or not to add locale suffix to locations
     *
     * @return whether to add locale suffix to locations
	 * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="Whether or not to add locale suffixes to menu locations"
	 */
	public Boolean getAddLocaleSuffix() {
		return addLocaleSuffix;
	}

    /**
     * Sets whether to add locale suffix to locations
     *
     * @param addLocaleSuffix whether to add locale suffix to locations
     */
	public void setAddLocaleSuffix(Boolean addLocaleSuffix) {
		this.addLocaleSuffix = addLocaleSuffix;
	}

    /**
     * Returns whether to check roles for menu items and output only which for
     * which user has enough rights.
     *
     * @return whether to check roles
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="Whether or not to check roles for menu items"
     */
    public Boolean getCheckRoles() {
        return checkRoles;
    }

    /**
     * Sets whether to check roles for menu items.
     *
     * @param checkRoles value to set
     * @see #checkRoles
     */
    public void setCheckRoles(Boolean checkRoles) {
        this.checkRoles = checkRoles;
    }

    /**
     * Returns whether to output only menu items for which hreflang is empty
     * or matches current language.
     *
     * @return whether to apply filter on hreflang
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Boolean"
	 * description="Whether or not to apply filter on hreflang to menu items"
     */
    public Boolean getHreflangFilter() {
        return hreflangFilter;
    }

    /**
     * Sets whether to apply filter on hreflang.
     *
     * @param hreflangFilter value to set
     */
    public void setHreflangFilter(Boolean hreflangFilter) {
        this.hreflangFilter = hreflangFilter;
    }

    /**
     * Returns number of slices.
     *
     * @return number of slices
     * @jsp.attribute required="false"
	 * rtexprvalue="true"
	 * type="java.lang.Integer"
	 * description="Number of slices to which to divide result"
     */
    public Integer getSlices() {
        return slices;
    }

    /**
     * Sets number of slices.
     *
     * @param slices number to set
     */
    public void setSlices(Integer slices) {
        this.slices = slices;
    }

    /**
     * Processes tag
     *
     * @throws JspException thrown if some error occured
     */
	public void doTag() throws JspException {

		PageContext pageContext = (PageContext) getJspContext();

		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
		ServletContext servletContext = request.getSession().getServletContext();

		applicationCtx = WebApplicationContextUtils.getRequiredWebApplicationContext(pageContext.getServletContext());

		if ( addLocaleSuffix == null ) {
			addLocaleSuffix = Boolean.TRUE;
		}

        if (checkRoles == null) {
            checkRoles = Boolean.FALSE;
        }

        if (hreflangFilter == null) {
            hreflangFilter = Boolean.FALSE;
        }

        //get locale
		if ( locale == null ) {
			Locale l = (Locale) pageContext.getAttribute(Globals.LOCALE_KEY, PageContext.SESSION_SCOPE);
			if ( l != null ) {
				locale = l.getLanguage();
			}
		}
		if ( locale == null ) {
			locale = Locale.getDefault().getLanguage();
		}

		// get definition
		TagUtils tagUtils = TagUtils.getInstance();
		if ( definition != null ) {
			ComponentDefinition tmpDef = null;
			try {
				tmpDef = TilesUtil.getDefinition(definition, request, servletContext);
			} catch ( Exception ex ) {
				//do nothing
			}
			if ( tmpDef == null ) {
				String errorMessage = "Specified definition '" + definition + "' was not found";
				if ( log.isErrorEnabled() ) {
					log.error(errorMessage);
				}
				JspTagException e = new JspTagException(errorMessage);
				tagUtils.saveException(pageContext, e);
				throw e;
			}
		} else {
			definition = (String) pageContext.getAttribute(ContentTilesRequestProcessor.DEFINITION_NAME, PageContext.REQUEST_SCOPE);
		}
		if ( definition == null ) {
			String errorMessage = "DEFINITION_NAME attribute was not found in request. This can be used only on pages processed by action using tile definition.";
			if ( log.isErrorEnabled() ) {
				log.error(errorMessage);
			}
			JspTagException e = new JspTagException(errorMessage);
			tagUtils.saveException(pageContext, e);
			throw e;
		}

		//get uri
		if ( uri != null ) {
			PageManager pageManager = (PageManager) applicationCtx.getBean(Constants.PAGE_MANAGER_BEAN);
			Page page = pageManager.findPageByUri(uri);
			if ( page == null ) {
				String errorMessage = "Specified page uri '" + uri + "' was not found";
				if ( log.isErrorEnabled() ) {
					log.error(errorMessage);
				}
				JspTagException e = new JspTagException(errorMessage);
				tagUtils.saveException(pageContext, e);
				throw e;
			}
		} else {
			uri = (String) pageContext.getAttribute(ContentTilesRequestProcessor.PROCESSED_URI, PageContext.REQUEST_SCOPE);
		}
		if ( uri == null ) {
			String errorMessage = "PROCESSED_URI attribute was not found in request. This can be used only on pages processed by action using tile definition.";
			if ( log.isErrorEnabled() ) {
				log.error(errorMessage);
			}
			JspTagException e = new JspTagException(errorMessage);
			tagUtils.saveException(pageContext, e);
			throw e;
		}

		CacheUtil cacheUtil = CacheUtil.getInstance(request);

        MenuUtil menuUtil = new MenuUtil((HttpServletRequest) pageContext.getRequest());
        MenuComponent menu = null;

		if ( log.isDebugEnabled() ) {
			log.debug("Try to get from cache: menuName=" + getName() + " locale=" + locale + " definition=" + definition + " uri=" + uri);
		}
		menu = cacheUtil.getMenuFromCache(getName(), locale, definition, uri);
		if ( menu == null ) {
			if ( log.isDebugEnabled() ) {
				log.debug("Trying to get from database");
			}
			try {
				menu = menuUtil.getMenuComponent(getName(), locale, definition, uri, true);
				if ( menu != null ) {
					cacheUtil.putMenuInCache(menu, getName(), locale, definition, uri);
				}
			} catch ( MenuUtilException e ) {
				throw new JspTagException(e);
			}
		}

		if ( menu != null ) {
            // making copy
            menu = menuUtil.recursiveCopy(menu);
            try {

				// set the location value to use
				// the context relative page attribute
				// if specified in the menu
				try {
					setPageLocation(menu);
				} catch ( MalformedURLException m ) {
					if ( log.isErrorEnabled() ) {
						log.error("Incorrect action or forward: " + m.getMessage());
						log.error("Menu '" + menu.getName() + "' location set to #");
					}
					menu.setLocation("#");
				}

                if (checkRoles.booleanValue()) {
                    removeNotAllowedMenuItems(menu, request);
                }

                if (hreflangFilter.booleanValue()) {
                    doHreflangFilter(menu, request);
                }

                Object result = splitToSlicesIfNeeded(menu, request);

				String attr = new String(name);

				if ( var != null ) {
					attr = new String(var);
				}

				int varScope = PageContext.PAGE_SCOPE;
				if ( scope != null ) {
					varScope = tagUtils.getScope(scope);
				}

				pageContext.setAttribute(attr, result, varScope);

			} catch ( Exception ex ) {
				if ( log.isErrorEnabled() ) {
					log.error(ex);
				}
				tagUtils.saveException(pageContext, ex);
				throw new JspException(ex);
			}
		} else {

			// if menu is null, perhaps it was not found, or it is invisible on current layer. So, do nothing,
			// just post the message to log

			if ( log.isInfoEnabled() ) {
				log.info("Menu '" + getName() + "' either does not exist, or it is invisible on current layer. Skipping...");
			}
		}
	}

    /**
	 * Sets the value for the menu location to the
	 * appropriate value if location is null.  If location
	 * is null, and the page attribute exists, it's value
	 * will be set to the the value for page prepended with
	 * the context path of the application.
	 * <p/>
	 * If the page is null, and the forward attribute exists,
	 * it's value will be looked up in struts-config.xml.
	 *
	 * @param menu The menu component to set the location for.
	 */
	protected void setPageLocation(MenuComponent menu)
	        throws MalformedURLException, JspException {

		PageContext pageContext = (PageContext) getJspContext();
		HttpServletRequest request =
		        (HttpServletRequest) pageContext.getRequest();
        HttpServletResponse response =
                (HttpServletResponse) pageContext.getResponse();
		setLocation(menu);
		String url = menu.getLocation();

		// Check if there are parameters on the value
		if ( (url != null) && (url.indexOf("${") > -1) ) {
			String queryString = null;

			if ( url.indexOf("?") > -1 ) {
				queryString = url.substring(url.indexOf("?") + 1);
				url = url.substring(0, url.indexOf(queryString));
			}

			StringBuffer sb = new StringBuffer();

			// variable is in the URL
			if ( queryString != null ) {
				sb = parseString(queryString, request);
				menu.setUrl(response.encodeURL(url + sb.toString()));
			} else {
				// parse the URL, rather than the queryString
				sb = parseString(url, request);
				menu.setUrl(response.encodeURL(sb.toString()));
			}
		} else {
            if (!GenericValidator.isBlankOrNull(url)) {
                menu.setUrl(response.encodeURL(url));
            } else {
                menu.setUrl(url);
            }
        }

		// do all contained menus
		MenuComponent[] subMenus = menu.getMenuComponents();

		if ( subMenus.length > 0 ) {
			for ( int i = 0; i < subMenus.length; i++ ) {
				setPageLocation(subMenus[i]);
			}
		}
	}

	protected void setLocation(MenuComponent menu) throws MalformedURLException {

		PageContext pageContext = (PageContext) getJspContext();
		HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
		HttpServletResponse response = (HttpServletResponse) pageContext.getResponse();

		// if the location attribute is null,
		// then set it with a context relative page
		// attribute if it exists
		if ( menu.getLocation() == null ) {
			try {
				if ( menu.getAnchor() != null ) {
					// are we sure we'll get an HttpServletRequest?
					menu.setLocation(request.getContextPath() +
					        getAnchor(menu.getAnchor()));
				} else if ( menu.getForward() != null ) {
					String forward = WebappUtil.computeURL(null, menu.getForward(), null, null, null, request, response, true, false, addLocaleSuffix.booleanValue(), true);
					menu.setLocation(forward);
				} else if ( menu.getAction() != null ) {
					// generate Struts Action URL,
					// this will append Context Path (if any),
					// Servlet Mapping (path mapping or extension mapping)
					// Module Prefix (if any) & Session ID (if any)
					String action = WebappUtil.computeURL(menu.getAction(), null, null, null, null, request, response, true, false, addLocaleSuffix.booleanValue(), true);
					menu.setLocation(action);
				}
			} catch ( NoClassDefFoundError e ) {
				if ( menu.getForward() != null ) {
					throw new MalformedURLException("forward '" + menu.getForward() + "' invalid - no struts.jar");
				} else if ( menu.getAction() != null ) {
					throw new MalformedURLException("action '" + menu.getAction() + "' invalid - no struts.jar");
				}
			}
		} else if ( addLocaleSuffix.booleanValue() && !menu.getExternalLocation().booleanValue() ) {
			// add locale suffix, if requested
			String href = menu.getLocation();
			if ( !WebappUtil.isAbsoluteURL(href) && !WebappUtil.isJavascriptURL(href) && !href.startsWith("/") ) {
				href = WebappUtil.computeURL(null, null, "/" + href, null, null, request, response, true, false, true);
				menu.setLocation(href);
			}
		}
	}

	/**
	 * Returns the value with page prepended with a "/"
	 * if it is not already.
	 *
	 * @param page The value for the page.
	 */
	protected String getAnchor(String page) {
		if ( page.startsWith("/") ) {
			return page;
		} else {
			page = "/" + page;
		}

		return page;
	}

	private StringBuffer parseString(String str, HttpServletRequest request) {

		PageContext pageContext = (PageContext) getJspContext();

		StringBuffer sb = new StringBuffer();

		while ( str.indexOf("${") >= 0 ) {
			sb.append(str.substring(0, str.indexOf("${")));

			String variable =
			        str.substring(str.indexOf("${") + 2, str.indexOf("}"));
			String value = (String) pageContext.findAttribute(variable);

			if ( value == null ) {
				// look for it as a request parameter
				value = request.getParameter(variable);
			}

			// is value still null?!
			if ( value == null ) {
				log.warn("Value for '" + variable +
				        "' not found in pageContext or as a request parameter");
			}

			sb.append(value);
			str = str.substring(str.indexOf("}") + 1, str.length());
		}

		return sb.append(str);
	}

    /**
     * Retains only menu items which are allowed to current user.
     *
     * @param menu initial menu component
     * @param request request
     */
    protected void removeNotAllowedMenuItems(MenuComponent menu,
                                             HttpServletRequest request) {
        List components = menu.getComponents();
        for (Iterator i = components.iterator(); i.hasNext(); ) {
            MenuComponent item = (MenuComponent) i.next();
            List roles = ConvertUtil.convertStringToList(item.getRoles(), ",",
                                                         true);
            boolean allowed = WebappUtil.isUserInRole(roles, false, request);
            if (allowed) {
                removeNotAllowedMenuItems(item, request);
            } else {
                i.remove();
            }
        }
    }

    /**
     * Retains only menu items which hreflang is empty or matches current
     * language.
     *
     * @param menu initial menu component
     * @param request request
     */
    protected void doHreflangFilter(MenuComponent menu,
                                    HttpServletRequest request) {
        Locale locale = (Locale) request.getSession().getAttribute(Globals.LOCALE_KEY);
        String language = locale.getLanguage();
        List components = menu.getComponents();
        for (Iterator i = components.iterator(); i.hasNext(); ) {
            MenuComponent item = (MenuComponent) i.next();
            String hreflang = item.getHreflang();
            boolean allowed = GenericValidator.isBlankOrNull(hreflang)
                    || hreflang.equals(language);
            if (allowed) {
                doHreflangFilter(item, request);
            } else {
                i.remove();
            }
        }
    }

    /**
     * <p>If <b>slices</b> is specified, produces list containing specified
     * number of fragments as of MenuComponent instances.</p>
     * <p>If <b>slices</b> is not specified, given menu is returned.</p>
     *
     * @param menu menu to process
     * @param request request
     * @return initial menu or list of slices
     * @throws IllegalAccessException may be thrown when copying properties
     * @throws InvocationTargetException may be thrown when copying properties
     */
    protected Object splitToSlicesIfNeeded(MenuComponent menu,
                                           HttpServletRequest request)
            throws IllegalAccessException, InvocationTargetException {
        Object result;
        if (slices != null) {
            result = splitToSlices(menu, slices.intValue());
        } else {
            result = menu;
        }
        return result;
    }

    /**
     * Splits a menu to a given number of slices.
     *
     * @param menu menu to split
     * @param numberOfSlices how many slices are needed
     * @return list of slices
     * @throws IllegalAccessException may be thrown when copying properties
     * @throws InvocationTargetException may be thrown when copying properties
     */
    protected List splitToSlices(MenuComponent menu, int numberOfSlices)
            throws IllegalAccessException, InvocationTargetException {
        List components = menu.getComponents();
        int totalSize = components.size();

        int baseSliceSize = totalSize / numberOfSlices;
        int remainder = totalSize % numberOfSlices;

        List result = new ArrayList(numberOfSlices);
        int baseIndex = 0;
        for (int i = 0; i < numberOfSlices; i++) {
            MenuComponent slice = new MenuComponent();
            WebappUtil.copyProperties(slice, menu);
            for (int j = 0; j < baseSliceSize; j++) {
                MenuComponent component = (MenuComponent) components.get(baseIndex + j);
                slice.addMenuComponent(component);
            }
            baseIndex += baseSliceSize;
            // we possibly need to add one more component to this slice
            if (remainder > 0) {
                MenuComponent component = (MenuComponent) components.get(baseIndex);
                slice.addMenuComponent(component);
                baseIndex++;
                remainder--;
            }
            result.add(slice);
        }

        return result;
    }
}
